package cn.tedu.dimanage.message.webapi.security;

import cn.tedu.dimanage.common.web.JsonResult;
import cn.tedu.dimanage.common.web.State;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;


/**
 * @Classname: JwtAuthenticationFilter
 * @Author: bromide
 * @CreateTime: 2022/6/25--12:48
 * @Version: V1.0
 * @Description:
 * 这个过滤器的执行需要在SpringSecurity之前
 * 否则SpringSecurity执行过滤的时候即使携带类jwt也会跳转到登陆页面
 *
 *
 * JWT过滤器,从请求头的Authorization中获取JWT中存入的用户信息,并添加到Spring Security的上下文中
 * 以至于Spring Security后续的组件(包括过滤器等)能从上下文中获取此用户的信息,从而验证是否登陆过,或者是否具有权限等
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    /**
     * JWT数据的密钥
     */
    @Value("${dimanage.jwt.secret-key}")
    private String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 清除Spring Security上下文中的数据
        // 避免此前曾经存入过用户信息,后续即使没有携带JWT,在Spring Security仍保存有上下文数据(包括用户信息)
        SecurityContextHolder.clearContext();


        // 客户端提交请求时,必须在请求头的Authentication中添加JWT数据,这是当前服务器程序的规定,客户端必须遵守
        // 尝试获取JWT数据
        String jwt = request.getHeader("Authorization");
        System.out.println("从请求头中获取到的JWT=" + jwt);
        // 判断是否存在jwt数据
        // 相当于     jwt == null & jwt.trim().equals("")
        if (!StringUtils.hasText(jwt)){
            // 如果不存在JWT数据,则放行,后续还有其他过滤器及相关逐渐进行其他处理,例如未登陆则要求登陆等
            // 此处不应该直接阻止运行,因为"登陆"、"注册"等请求本应该没有jwt数据
            System.out.println("请求头中无JWT数据,当前过滤器放行");
            filterChain.doFilter(request,response);  // 放行,继续执行过滤器链中后续的过滤器
            return;
        }
        // 注意: 此事执行时,如果请求头中鞋材类Authentication,日志中将输入,且不会有任何响应,
        // 以下代码时有可能抛出异常的
        // 密钥和各个key应该统一定义

        String username = null;
        String roleIdString = null;
        try {
            System.out.println("请求头中包含JWT,准备解析此数据");
            Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
            username = claims.get(JwtConst.KEY_USERNAME).toString();
            roleIdString = claims.get(JwtConst.KEY_ROLEID).toString();
            System.out.println("username=" + username);
            System.out.println("roleIdString=" + roleIdString);
        }catch (ExpiredJwtException e){
            System.out.println("解析JWT失败,此JWT数据已过期: " + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_EXPIRED,"您的登陆已过期,请重新登陆!"
            );
            // 将结果转换成json,并响应
            String jsonString = JSON.toJSONString(jsonResult);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }catch (MalformedJwtException e){
            // 将登陆生成的JWT修改之后会导致
            System.out.println("解析JWT失败,此JWT数据错误,无法解析: " + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_MALFORMED,"获取登陆信息失败,请重新登陆!"
            );
            // 将结果转换成json,并响应
            // (之前都是springMvc帮助我们做的,过滤器的执行在所有控制类之前,所以程序执行到这里的时候SpringMvc还没有执行,所以需要我们调用FastJson并用流返回给客户端)
            String jsonString = JSON.toJSONString(jsonResult);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }catch (SignatureException e){
            // 密钥不一致导致的
            System.out.println("解析JWT失败,JWT签名错误: " + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_SIGNATURE,"获取登陆信息失败,请重新登陆!"
            );
            // 将结果转换成json,并响应
            String jsonString = JSON.toJSONString(jsonResult);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }catch (Exception e){
            // 补全异常,避免程序抛出没有考虑到的异常类型
            System.out.println("解析JWT失败,异常类型: " + e.getClass().getName());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_INTERNAL_SERVER_ERROR,"获取登陆信息失败,请重新登陆!"
            );
            // 将结果转换成json,并响应
            String jsonString = JSON.toJSONString(jsonResult);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }

        // 将此前从JWT中读取到的roleId(JSON字符串)转换成Collection<? extends org.springframework.security.core.GrantedAuthority>
        List<SimpleGrantedAuthority> roleId = JSON.parseArray(roleIdString, SimpleGrantedAuthority.class);
        // 将解析得到的用户信息传递给SpringSecurity
        // 获取Spring Security的上下文,并将Authentication放到上下文中
        // 在Authentication中封装: 用户名,null,权限列表(角色)
        // 因为接下来并不会处理认证,所以Authentication中不需要密码
        // 后续,Spring Security发现上下文中有Authentication,就会视为已登陆,甚至可以获得相关信息
        Authentication authentication = new UsernamePasswordAuthenticationToken(username,null,roleId);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 放行
        filterChain.doFilter(request,response);
    }
}

